 /************************************************************************************************************
  *  DraggableTable is a javascript objkect that allow you to apply drag n' drop behaviour to a table's cells.
  *  
  *  Copyright (C) 2010  Brian Couchman
  *
  *  This program is free software: you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
  *  the Free Software Foundation, either version 3 of the License, or
  *  (at your option) any later version.
  *
  *  This program is distributed in the hope that it will be useful,
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
***/

var DraggableTable = {
			X_OFFSET: 30,
			Y_OFFSET: 0,
			row: null,
			draggedRowGhost: null,
			triggers: [],
			placeHolder: null, 

			/**
			 * Turn a regular table into a draggable table.
			 * Options are : 
			 *  - id: id of the table
			 *  - handle: class name of the handle elements
			 *  - external events (onStart, onMove, onUpdate, onEnd)
			 *
			 */
			create: function(options){
				try{
					this.options = {
						handle: null,				// dragging is active only on element with this class name
						only: null,					// apply dragging facilities to TR with this calss name only
						keepInside: false,			// keep dragging inside the table
						verticalConstraint: true,	// disable horizontal moves (vertical only)
						onStart: null,
						onMove: null,
						onUpdate: null,
						onEnd: null
					}
					Object.extend(this.options, options || {});

					this.table = $(this.options.id);
					/*
					var handles;
					if(this.options.handle){
						handles = this.table.select("." + this.options.handle);
					}else{
						handles = this.table.select("tbody tr");
					}
					handles.each(function(handle){
						handle.observe("mousedown", this.startDrag.bindAsEventListener(this));
						//handle.observe("mouseup", this.dropTR.bindAsEventListener(this));
					}.bind(this))
					*/
					Event.observe(this.table, "mousedown", this.startDrag.bindAsEventListener(this));

					this.updateTableRows();

					
					//Build the thresholds where the rows shift (the threshold is the middle vertically of each row)
					this.triggers = this.rows.map(function(row, index){
						var top = row.cumulativeOffset()[1];
						return top + (row.getHeight() / 2);
					});

				}catch(e) { log.error("create: >> " + (e.description||e)); }
			},

			/**
			 * Lists the rows of the table, and return an array with it, in the right order
			 */
			updateTableRows: function(){
				//Build table draggable rows array
				this.rows = this.options.only ? this.table.select('tbody tr.' + this.options.only) : this.table.select('tbody tr');

				//If dragging is constrained inside the table, calculate table dimensions
				if(this.options.keepInside){
					this.tableDims = {
						top: this.rows.first().cumulativeOffset().top,
						bottom: this.rows.last().cumulativeOffset().top + this.rows.last().getHeight()
					};
				}
			},

			/**
			 * Called when a handle (or a draggable row if no handle is defined) is clicked.
			 * It initiates the dragging process (building ghost, setting place holder, setting observers)
			 *
			 * Called on  click on any draggable element.
			 */
			startDrag: function(event){
				//log.warn("startDrag");
				try{
					//Get the clicked element, and check if it fits with the handle rule. If not, do nothing.
					var clickedObject = event.element();
					if(this.options.handle && !clickedObject.hasClassName(this.options.handle)) return;

					//Avoid the selection effect on drag
					if(event.preventDefault) event.preventDefault();
					
					//Get the dragged row
					this.row = clickedObject.tagName.toLowerCase() == 'tr' ? clickedObject : clickedObject.up('tr');
					this.rowHeight = this.row.getHeight();

					this.table.addClassName('nohover');
				
					//Build the ghost row
					this.buildGhost(event);
					
					//Build the place holder, and insert it just before the dragged row...
					var colspan = this.row.select('td').size();
					this.placeHolder = new Element("TR", {className: "dragPH"}).insert(new Element("TD", {colspan: colspan, style:"height: " + this.row.getHeight() + "px;"}).update('&#160;'));
					this.placeHolder.style.display = CoradiantUI.browser.IE ? "block" : "table-row";
					this.row.insert({before: this.placeHolder});


					//...and hide the dragged row (use the ghost). This instruction must happen AFTER the insertion of the place holder.
					this.row.style.display = "none";

					//bound version of the functions -> cached to allow Event.stopObserving		
					this._moveTR = this.moveTR.bindAsEventListener(this); 
					this._dropTR = this.dropTR.bindAsEventListener(this);

					//Add observers
					Element.observe(document, "mousemove", this._moveTR);
					Element.observe(document, "mouseup", this._dropTR);

					//Execute external event onStart.
					if(this.options.onStart && typeof this.options.onStart == "function"){
						this.options.onStart.apply();
					}

				}catch(e) { log.error("startDrag: >> " + (e.description||e)); }
			},
				

			/**
			 * Builds a copy of the dragged row, called a ghost.
			 *
			 * Called by DraggableTR.startDrag
			 */
			buildGhost: function(event){
				try{
					var rowOffset = this.row.cumulativeOffset();
					var clonedRow = this.row.cloneNode(true);
					
					var divStyle = {
						position: 'absolute', 
						width: this.table.getWidth() + 'px'
					};
					/*var tableStyle = {
						width: '100%', 
						border: '2px solid red;',
						backgroundColor: this.row.getStyle('background-color') ? this.row.getStyle('background-color') : '#FFF'
					};*/
					var tableStyle = "width: 100%; background-color: #FFF;height: " + this.row.getHeight() + "px;";
						
					this.draggedRowGhost = new Element("DIV", {id: 'draggedRowGhost'}).setStyle(divStyle);
					//this.draggedRowGhost.insert(new Element("TABLE", {className : this.table.classNames()}).insert(new Element("TR").insert(clonedRow.innerHTML)));
					this.draggedRowGhost.insert("<TABLE class='" + this.table.classNames() + "' style='" + tableStyle + "'><TR class='" + this.row.classNames() + "' style='height: " + this.row.getHeight() + "px;'>" + clonedRow.innerHTML + "</TR></TABLE>");

					//Initial positionning (X_OFFSET right of the row horizontal position for X, and mouse position for Y)
					var x = this.options.verticalConstraint ? this.row.getStyle('left') : event.pointerX();
					this.draggedRowGhost.setStyle({left: (x + this.X_OFFSET) + 'px', top: event.pointerY() + 'px'});

					//Set ghost opacity
					this.draggedRowGhost.setOpacity(0.8);

					//Insert ghost 
					//Element.insert(document.body, {top: new Element("div", {style: "position: absolute; border: 2px solid red;", id: "holder"})});
					Element.insert(document.body, {top: this.draggedRowGhost});
				}catch(e) { log.error("buildGhost >> " + (e.description||e)); }
			},


			/** 
			 * Moves the ghost row, and detect if a threshold has been crossed.
			 *
			 * Called on every mouse move once the dragging process has been started.
			 */
			moveTR: function(event){
				//log.warn("moveTR");
				try{
					//Avoid the selection effect on drag
					if(event.preventDefault) event.preventDefault();
					
					//Move the ghost (draggedRowGhost)
					// y stands for the mouse position, ghostY is the position the ghost should be set at.
					var y = event.pointerY(); var ghostY = y;
					//If dragging is constrained inside table, ghostY should be constrained.
					if(this.options.keepInside){ 
						if(y <= this.tableDims.top) ghostY = this.tableDims.top;
						if(y >= (this.tableDims.bottom - this.rowHeight)) ghostY = this.tableDims.bottom - this.rowHeight;
					}
					
					this.draggedRowGhost.setStyle({top: ghostY - DraggableTable.Y_OFFSET + 'px'});
					if(!this.options.verticalConstraint){ //If dragging is free, move also on the horizontal axis
						this.draggedRowGhost.setStyle({left: event.pointerX() + 'px'});
					}
					
					//Detect when the mouse cross a threshold, and move the place holder
					//Use mouse position (y), because ghost position (ghostY) can be constrained
					this.triggers.each(function(trigger, index){
						if(y < this.triggers[index]) {
							if(this.curpos != index) this.movePlaceHolder(index);
							throw $break;
						}
					}.bind(this));
					
					if( y > this.triggers.last()){
						if(this.curpos != this.triggers.size()) this.movePlaceHolder(this.triggers.size());
					}

					//Execute external event onMove.
					if(this.options.onMove && typeof this.options.onMove == "function"){
						this.options.onMove.apply();
					}

					return false;
					
				}catch(e) { log.error("moveTR >> " + (e.description||e)); }
				
			},


			/** 
			 * Moves the placeholder (green row) to the place where the row would be dropped if mouse would be released
			 *
			 * Called by DraggableTR.moveTR when a threshold is crossed.
			 */
			movePlaceHolder : function(targetIndex){
				//log.warn("movePlaceHolder");
				try{
					this.curpos = targetIndex;

					if(targetIndex < this.rows.size()){
						//Insert place holder before the hovered row
						var nextRow = this.rows[targetIndex];
						if(nextRow != this.row) {
							nextRow.insert({before: this.placeHolder});
						}else{
							//nothing to do here
						}
					}else{
						//Insert place holder at last position
						var afterRow = this.rows.last();
						if(afterRow != this.row) afterRow.insert({after: this.placeHolder});
					}

					//Execute external event onUpdate.
					if(this.options.onUpdate && typeof this.options.onUpdate == "function"){
						this.options.onUpdate.apply();
					}

					return false;
				}catch(e) { log.error("movePlaceHolder: >> " + (e.description||e)); }
				
			},

			/**
			 * End the dragging process and put the dragged row at the position of the place holder.
			 *
			 * Called by a mouse release once the dragging process has been started.
			 */
			dropTR: function(event){
				//log.warn("dropTR");
				try{
					//Stop observing mousemove and mouseup, the dragging is over.
					Element.stopObserving(document, "mousemove", this._moveTR);
					Element.stopObserving(document, "mouseup", this._dropTR);
					
					//Make the dragged row (the real one, not the ghost) visible,
					//And insert it at the placeholder's position. Then remove placeholder.
					this.row.style.display = CoradiantUI.browser.IE ? "block" : "table-row";
					if(this.row) {
						if(this.placeHolder) {
							this.placeHolder.insert({before: this.row}); 
							this.placeHolder.remove();
						}
					}
					
					//Remove ghost.
					if(this.draggedRowGhost) this.draggedRowGhost.remove();

					this.table.removeClassName('nohover');

					//Reset placeholder position cache.
					this.curpos = -1;

					//Order have changed, the rows array must be updated.
					this.updateTableRows();

					//Execute external event onEnd.
					if(this.options.onEnd && typeof this.options.onEnd == "function"){
						this.options.onEnd.apply();
					}

					return false;
				}catch(e) { log.error("dropTR: >> " + (e.description||e)); }
			}
		}
